package queue;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;

import page.PageRetriever;
import data.DataGatherer;

/**
 * Queue which holds the URLs.  Ensures no duplicate URLs will be added.
 * 
 * Multi-threaded.
 * 
 * Creates pop-up threads as items are added.
 * 
 * Thread-safe! Allows multiple threads to add to the queue using locks.
 * 
 * @author Ken "David" McClain
 * @author Peter Mosher
 * @version 1
 */
@SuppressWarnings("serial")
public class PageToRetrieve extends LinkedList<String> implements Runnable {
	
	/** 
	 * Collection of URLs that have already been added to the queue previously.
	 * This field keeps track of URLs so we will not add duplicates.
	 * Primarily used by the add() method.
	 */
	private final Map<String, Boolean> alreadyAdded = new HashMap<String, Boolean>();
	
	/** Maximum number of popup threads we are allowed to generate. */
	private final int maxNumberOfThreads;
	
	/** List of page retriever threads. */
	private PageRetriever[] pageRetrievers;
	
	/** Queue to enqueue retrieved pages. */
	private PageBuffer pageBuffer;
	
	private DataGatherer dataGatherer;
	
	/** Thread waits on this object when the queue is empty (no URLs to retrieve).  */
	private BlockingObject emptyBlock = new BlockingObject();

	/** Thread waits on this object when no threads are available. */
	private BlockingObject threadsFullBlock = new BlockingObject();

	/** Thread waits on this object when all threads are currently running. */
	private BlockingObject threadFinishedBlock;
	
	/** Flag for stopping the creation of pop-up threads. */
	private boolean stop = false;
	
	/**
	 * Constructor. Initializes threads that will be run.
	 * @param threadNum
	 */
	public PageToRetrieve(int threadNum, DataGatherer dg) {
		super();
		
		// Initialize PageRetriever threads.
		maxNumberOfThreads = threadNum;
		
		// Set the data gatherer object.
		dataGatherer = dg;
	}
	
	/**
	 * Sets the PageBuffer object. Also instantiates the PageRetriever threads.
	 * This is a separate method because
	 * @param pb Page queue to enqueue retrieves pages into.
	 */
	public void setPageBuffer(PageBuffer pb) {
		pageBuffer = pb;
		pageRetrievers = new PageRetriever[maxNumberOfThreads];
		for (int i = 0; i < pageRetrievers.length; i++) 
			pageRetrievers[i] = new PageRetriever(pageBuffer);
		
	}
	
	/**
	 * Set the object to notify when threads finish executing.
	 * This object is waited on by ModelMulti, which decides when to stop.
	 * @param block The object to notify when threads finishe executing.
	 */
	public void setFinishedBlock(final BlockingObject block) {
		threadFinishedBlock = block;
	}
	
	
	/** 
	 * Thread-safe method to add a URL to the queue.
	 * Ensures that URLs will only be added once. 
	 * 
	 * @param url The URL to add.
	 * @return False if unable to add (or URL has already been added), 
	 * 			otherwise returns true.
	 */
	synchronized public boolean add(final String url) {
		boolean result;
		// Check if this URL has already been added before.
		if (alreadyAdded.containsKey(url))
			return false;
		
		// Add URL to list of URLs already added.
		alreadyAdded.put(url, true);
		result = super.add(url);

		// Wake up the current thread in case it was blocking on an empty queue.
		synchronized (emptyBlock) {
			emptyBlock.notify();
		}
		
		return result;
	}
	
	/** @return True if queue is empty, false otherwise. Thread-safe. */
	synchronized public boolean isEmpty() { return super.isEmpty(); }
	
	
	/**
	 * Stops this thread from creating more pop-up threads.
	 */
	public void stop() { 
		stop = true;
		
		// Wake up the current thread (just in case it was blocking).
		synchronized (emptyBlock) {
			emptyBlock.notify();
		}
		
		synchronized (threadsFullBlock) {
			threadsFullBlock.notify();
		}
	}
	
	
	/**
	 * Informs ModelMulti if the threads are busy or the queue is empty.
	 * @return True if the queue is empty AND all threads are not 'busy'.
	 * 		False otherwise.
	 */
	synchronized public boolean isStopped() {
		if (!isEmpty()) {
			return false;
		}
		
		for (PageRetriever pr : pageRetrievers) {
			if (pr.isBusy()) {
				return false;
			}
		}
		
		return true;
	}
	
	
	/**
	 * Threaded. Scans for new items, 
	 * Starts new threads when there are threads available.
	 */
	public void run() {
		
		// While our flag does not say to stop and
		// the DataGatherer has not reached the page limitl,
		while (!stop && !dataGatherer.hitLimit()) {
			
			/* This is the old implementation which utilizes SPIN-LOCKING and is BAD.
			if (isEmpty())
				continue;
			*/
			
			// If queue is empty, we "block".
			if (isEmpty()) {
				// Tells thread to wait until someone calls "notify()".
				try {
					synchronized (emptyBlock) {
						emptyBlock.wait();
					}
				} catch (InterruptedException ie) { ie.printStackTrace(); }
			}
			
			// If we were blocking and are now going to stop, exit the loop.
			if (stop) 
				break;
			
			// At this point, we have something in the queue to grab.
			
			// Find if there are any threads that are available...
			int availableThread = -1;
			while (availableThread == -1 && !stop) {
				for (int i = 0; i < pageRetrievers.length; i++) {
					if (!pageRetrievers[i].isBusy()) {
						availableThread = i;
						break;
					}
				}
				
				// If there are no threads available, BLOCK!
				if (availableThread == -1) {
					try {
						synchronized (threadsFullBlock) {
							threadsFullBlock.wait();
						}
					} catch (InterruptedException ie) { }
					// The code will halt execution here until "threadsFullBlock" is notified
					// Which happens when a thread completes execution.
				}
				
			}
			
			// If we were blocking, were woken up, but it's time to stop, then break.
			if (stop)
				break;
			
			try {
				// Create the new thread (pop-up thread).
				pageRetrievers[availableThread] = new PageRetriever(pageBuffer);
				
				// Set the thread as "busy"
				pageRetrievers[availableThread].setBusy(true);
				
				// Set the URL
				pageRetrievers[availableThread].setURL(remove());
				
				// Set the object to notify when the thread completes. (notifies this thread).
				pageRetrievers[availableThread].setBlockingObject(threadsFullBlock);
				
				// Set the object to notify when the thread completes. (notifies ModelMulti thread).
				pageRetrievers[availableThread].setBlockedUntilFinished(threadFinishedBlock);
				
				// Start the new thread.
				pageRetrievers[availableThread].start();
				
			} catch (NoSuchElementException nsee) { } // Shouldn't happen
			
		} // End of big While loop
		
		// Join with threads, so we wait until they finish.
		for (PageRetriever pr : pageRetrievers) {
			try {
				pr.join();
			} catch (InterruptedException ie) { }
		}
		
	}
	
}
